/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

static const Identifier tableColumnProperty { "_tableColumnId" };
static const Identifier tableAccessiblePlaceholderProperty { "_accessiblePlaceholder" };

class TableListBox::RowComp final : public TooltipClient,
                                    public ComponentWithListRowMouseBehaviours<RowComp>
{
public:
    explicit RowComp (TableListBox& tlb)
        : owner (tlb)
    {
        setFocusContainerType (FocusContainerType::focusContainer);
    }

    void paint (Graphics& g) override
    {
        if (auto* tableModel = owner.getTableListBoxModel())
        {
            tableModel->paintRowBackground (g, getRow(), getWidth(), getHeight(), isSelected());

            auto& headerComp = owner.getHeader();
            const auto numColumns = jmin ((int) columnComponents.size(), headerComp.getNumColumns (true));
            const auto clipBounds = g.getClipBounds();

            for (int i = 0; i < numColumns; ++i)
            {
                if (columnComponents[(size_t) i]->getProperties().contains (tableAccessiblePlaceholderProperty))
                {
                    auto columnRect = headerComp.getColumnPosition (i).withHeight (getHeight());

                    if (columnRect.getX() >= clipBounds.getRight())
                        break;

                    if (columnRect.getRight() > clipBounds.getX())
                    {
                        Graphics::ScopedSaveState ss (g);

                        if (g.reduceClipRegion (columnRect))
                        {
                            g.setOrigin (columnRect.getX(), 0);
                            tableModel->paintCell (g, getRow(), headerComp.getColumnIdOfIndex (i, true),
                                                   columnRect.getWidth(), columnRect.getHeight(), isSelected());
                        }
                    }
                }
            }
        }
    }

    void update (int newRow, bool isNowSelected)
    {
        jassert (newRow >= 0);

        updateRowAndSelection (newRow, isNowSelected);

        auto* tableModel = owner.getTableListBoxModel();

        if (tableModel != nullptr && getRow() < owner.getNumRows())
        {
            const ComponentDeleter deleter { columnForComponent };
            const auto numColumns = owner.getHeader().getNumColumns (true);

            while (numColumns < (int) columnComponents.size())
                columnComponents.pop_back();

            while ((int) columnComponents.size() < numColumns)
                columnComponents.emplace_back (nullptr, deleter);

            for (int i = 0; i < numColumns; ++i)
            {
                auto columnId = owner.getHeader().getColumnIdOfIndex (i, true);
                auto originalComp = std::move (columnComponents[(size_t) i]);
                auto oldCustomComp = originalComp != nullptr && ! originalComp->getProperties().contains (tableAccessiblePlaceholderProperty)
                                   ? std::move (originalComp)
                                   : std::unique_ptr<Component, ComponentDeleter> { nullptr, deleter };
                auto compToRefresh = oldCustomComp != nullptr && columnId == static_cast<int> (oldCustomComp->getProperties()[tableColumnProperty])
                                   ? std::move (oldCustomComp)
                                   : std::unique_ptr<Component, ComponentDeleter> { nullptr, deleter };

                columnForComponent.erase (compToRefresh.get());
                std::unique_ptr<Component, ComponentDeleter> newCustomComp { tableModel->refreshComponentForCell (getRow(),
                                                                                                                  columnId,
                                                                                                                  isSelected(),
                                                                                                                  compToRefresh.release()),
                                                                             deleter };

                auto columnComp = [&]
                {
                    // We got a result from refreshComponentForCell, so use that
                    if (newCustomComp != nullptr)
                        return std::move (newCustomComp);

                    // There was already a placeholder component for this column
                    if (originalComp != nullptr)
                        return std::move (originalComp);

                    // Create a new placeholder component to use
                    std::unique_ptr<Component, ComponentDeleter> comp { new Component, deleter };
                    comp->setInterceptsMouseClicks (false, false);
                    comp->getProperties().set (tableAccessiblePlaceholderProperty, true);
                    return comp;
                }();

                columnForComponent.emplace (columnComp.get(), i);

                // In order for navigation to work correctly on macOS, the number of child
                // accessibility elements on each row must match the number of header accessibility
                // elements.
                columnComp->setFocusContainerType (FocusContainerType::focusContainer);
                columnComp->getProperties().set (tableColumnProperty, columnId);
                addAndMakeVisible (*columnComp);

                columnComponents[(size_t) i] = std::move (columnComp);
                resizeCustomComp (i);
            }
        }
        else
        {
            columnComponents.clear();
        }
    }

    void resized() override
    {
        for (auto i = (int) columnComponents.size(); --i >= 0;)
            resizeCustomComp (i);
    }

    void resizeCustomComp (int index)
    {
        if (auto& c = columnComponents[(size_t) index])
        {
            c->setBounds (owner.getHeader()
                               .getColumnPosition (index)
                               .withY (0)
                               .withHeight (getHeight()));
        }
    }

    void performSelection (const MouseEvent& e, bool isMouseUp)
    {
        owner.selectRowsBasedOnModifierKeys (getRow(), e.mods, isMouseUp);

        auto columnId = owner.getHeader().getColumnIdAtX (e.x);

        if (columnId != 0)
            if (auto* m = owner.getTableListBoxModel())
                m->cellClicked (getRow(), columnId, e);
    }

    void mouseDoubleClick (const MouseEvent& e) override
    {
        if (! isEnabled())
            return;

        const auto columnId = owner.getHeader().getColumnIdAtX (e.x);

        if (columnId != 0)
            if (auto* m = owner.getTableListBoxModel())
                m->cellDoubleClicked (getRow(), columnId, e);
    }

    String getTooltip() override
    {
        auto columnId = owner.getHeader().getColumnIdAtX (getMouseXYRelative().getX());

        if (columnId != 0)
            if (auto* m = owner.getTableListBoxModel())
                return m->getCellTooltip (getRow(), columnId);

        return {};
    }

    Component* findChildComponentForColumn (int columnId) const
    {
        const auto index = (size_t) owner.getHeader().getIndexOfColumnId (columnId, true);

        if (isPositiveAndBelow (index, columnComponents.size()))
            return columnComponents[index].get();

        return nullptr;
    }

    int getColumnNumberOfComponent (const Component* comp) const
    {
        const auto iter = columnForComponent.find (comp);
        return iter != columnForComponent.cend() ? iter->second : -1;
    }

    std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
    {
        return std::make_unique<RowAccessibilityHandler> (*this);
    }

    TableListBox& getOwner() const { return owner; }

private:
    //==============================================================================
    class RowAccessibilityHandler final : public AccessibilityHandler
    {
    public:
        RowAccessibilityHandler (RowComp& rowComp)
            : AccessibilityHandler (rowComp,
                                    AccessibilityRole::row,
                                    getListRowAccessibilityActions (rowComp),
                                    { std::make_unique<RowComponentCellInterface> (*this) }),
              rowComponent (rowComp)
        {
        }

        String getTitle() const override
        {
            if (auto* m = rowComponent.owner.ListBox::model)
                return m->getNameForRow (rowComponent.getRow());

            return {};
        }

        String getHelp() const override  { return rowComponent.getTooltip(); }

        AccessibleState getCurrentState() const override
        {
            if (auto* m = rowComponent.owner.getTableListBoxModel())
                if (rowComponent.getRow() >= m->getNumRows())
                    return AccessibleState().withIgnored();

            auto state = AccessibilityHandler::getCurrentState();

            if (rowComponent.owner.multipleSelection)
                state = state.withMultiSelectable();
            else
                state = state.withSelectable();

            if (rowComponent.isSelected())
                return state.withSelected();

            return state;
        }

    private:
        class RowComponentCellInterface final : public AccessibilityCellInterface
        {
        public:
            RowComponentCellInterface (RowAccessibilityHandler& handler)
                : owner (handler)
            {
            }

            int getDisclosureLevel() const override  { return 0; }

            const AccessibilityHandler* getTableHandler() const override  { return owner.rowComponent.owner.getAccessibilityHandler(); }

        private:
            RowAccessibilityHandler& owner;
        };

    private:
        RowComp& rowComponent;
    };

    //==============================================================================
    class ComponentDeleter
    {
    public:
        explicit ComponentDeleter (std::map<const Component*, int>& locations)
            : columnForComponent (&locations) {}

        void operator() (Component* comp) const
        {
            columnForComponent->erase (comp);

            if (comp != nullptr)
                delete comp;
        }

    private:
        std::map<const Component*, int>* columnForComponent;
    };

    TableListBox& owner;
    std::map<const Component*, int> columnForComponent;
    std::vector<std::unique_ptr<Component, ComponentDeleter>> columnComponents;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComp)
};


//==============================================================================
class TableListBox::Header final : public TableHeaderComponent
{
public:
    Header (TableListBox& tlb)  : owner (tlb) {}

    void addMenuItems (PopupMenu& menu, int columnIdClicked) override
    {
        if (owner.isAutoSizeMenuOptionShown())
        {
            menu.addItem (autoSizeColumnId, TRANS ("Auto-size this column"), columnIdClicked != 0);
            menu.addItem (autoSizeAllId, TRANS ("Auto-size all columns"), owner.getHeader().getNumColumns (true) > 0);
            menu.addSeparator();
        }

        TableHeaderComponent::addMenuItems (menu, columnIdClicked);
    }

    void reactToMenuItem (int menuReturnId, int columnIdClicked) override
    {
        switch (menuReturnId)
        {
            case autoSizeColumnId:      owner.autoSizeColumn (columnIdClicked); break;
            case autoSizeAllId:         owner.autoSizeAllColumns(); break;
            default:                    TableHeaderComponent::reactToMenuItem (menuReturnId, columnIdClicked); break;
        }
    }

private:
    TableListBox& owner;

    enum { autoSizeColumnId = 0xf836743, autoSizeAllId = 0xf836744 };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Header)
};

//==============================================================================
TableListBox::TableListBox (const String& name, TableListBoxModel* const m)
    : ListBox (name, nullptr), model (m)
{
    ListBox::assignModelPtr (this);

    setHeader (std::make_unique<Header> (*this));
}

TableListBox::~TableListBox()
{
}

void TableListBox::setModel (TableListBoxModel* newModel)
{
    if (model != newModel)
    {
        model = newModel;
        updateContent();
    }
}

void TableListBox::setHeader (std::unique_ptr<TableHeaderComponent> newHeader)
{
    if (newHeader == nullptr)
    {
        jassertfalse; // you need to supply a real header for a table!
        return;
    }

    Rectangle<int> newBounds (100, 28);

    if (header != nullptr)
        newBounds = header->getBounds();

    header = newHeader.get();
    header->setBounds (newBounds);

    setHeaderComponent (std::move (newHeader));

    header->addListener (this);
}

int TableListBox::getHeaderHeight() const noexcept
{
    return header->getHeight();
}

void TableListBox::setHeaderHeight (int newHeight)
{
    header->setSize (header->getWidth(), newHeight);
    resized();
}

void TableListBox::autoSizeColumn (int columnId)
{
    auto width = model != nullptr ? model->getColumnAutoSizeWidth (columnId) : 0;

    if (width > 0)
        header->setColumnWidth (columnId, width);
}

void TableListBox::autoSizeAllColumns()
{
    for (int i = 0; i < header->getNumColumns (true); ++i)
        autoSizeColumn (header->getColumnIdOfIndex (i, true));
}

void TableListBox::setAutoSizeMenuOptionShown (bool shouldBeShown) noexcept
{
    autoSizeOptionsShown = shouldBeShown;
}

Rectangle<int> TableListBox::getCellPosition (int columnId, int rowNumber, bool relativeToComponentTopLeft) const
{
    auto headerCell = header->getColumnPosition (header->getIndexOfColumnId (columnId, true));

    if (relativeToComponentTopLeft)
        headerCell.translate (header->getX(), 0);

    return getRowPosition (rowNumber, relativeToComponentTopLeft)
            .withX (headerCell.getX())
            .withWidth (headerCell.getWidth());
}

Component* TableListBox::getCellComponent (int columnId, int rowNumber) const
{
    if (auto* rowComp = dynamic_cast<RowComp*> (getComponentForRowNumber (rowNumber)))
        return rowComp->findChildComponentForColumn (columnId);

    return nullptr;
}

void TableListBox::scrollToEnsureColumnIsOnscreen (int columnId)
{
    auto& scrollbar = getHorizontalScrollBar();
    auto pos = header->getColumnPosition (header->getIndexOfColumnId (columnId, true));

    auto x = scrollbar.getCurrentRangeStart();
    auto w = scrollbar.getCurrentRangeSize();

    if (pos.getX() < x)
        x = pos.getX();
    else if (pos.getRight() > x + w)
        x += jmax (0.0, pos.getRight() - (x + w));

    scrollbar.setCurrentRangeStart (x);
}

int TableListBox::getNumRows()
{
    return model != nullptr ? model->getNumRows() : 0;
}

void TableListBox::paintListBoxItem (int, Graphics&, int, int, bool)
{
}

Component* TableListBox::refreshComponentForRow (int rowNumber, bool rowSelected, Component* existingComponentToUpdate)
{
    if (existingComponentToUpdate == nullptr)
        existingComponentToUpdate = new RowComp (*this);

    static_cast<RowComp*> (existingComponentToUpdate)->update (rowNumber, rowSelected);

    return existingComponentToUpdate;
}

void TableListBox::selectedRowsChanged (int row)
{
    if (model != nullptr)
        model->selectedRowsChanged (row);
}

void TableListBox::deleteKeyPressed (int row)
{
    if (model != nullptr)
        model->deleteKeyPressed (row);
}

void TableListBox::returnKeyPressed (int row)
{
    if (model != nullptr)
        model->returnKeyPressed (row);
}

void TableListBox::backgroundClicked (const MouseEvent& e)
{
    if (model != nullptr)
        model->backgroundClicked (e);
}

void TableListBox::listWasScrolled()
{
    if (model != nullptr)
        model->listWasScrolled();
}

void TableListBox::tableColumnsChanged (TableHeaderComponent*)
{
    setMinimumContentWidth (header->getTotalWidth());
    repaint();
    updateColumnComponents();
}

void TableListBox::tableColumnsResized (TableHeaderComponent*)
{
    setMinimumContentWidth (header->getTotalWidth());
    repaint();
    updateColumnComponents();
}

void TableListBox::tableSortOrderChanged (TableHeaderComponent*)
{
    if (model != nullptr)
        model->sortOrderChanged (header->getSortColumnId(),
                                 header->isSortedForwards());
}

void TableListBox::tableColumnDraggingChanged (TableHeaderComponent*, int columnIdNowBeingDragged_)
{
    columnIdNowBeingDragged = columnIdNowBeingDragged_;
    repaint();
}

void TableListBox::resized()
{
    ListBox::resized();

    header->resizeAllColumnsToFit (getVisibleContentWidth());
    setMinimumContentWidth (header->getTotalWidth());
}

void TableListBox::updateColumnComponents() const
{
    auto firstRow = getRowContainingPosition (0, 0);

    for (int i = firstRow + getNumRowsOnScreen() + 2; --i >= firstRow;)
        if (auto* rowComp = dynamic_cast<RowComp*> (getComponentForRowNumber (i)))
            rowComp->resized();
}

template <typename FindIndex>
Optional<AccessibilityTableInterface::Span> findRecursively (const AccessibilityHandler& handler,
                                                             Component* outermost,
                                                             FindIndex&& findIndexOfComponent)
{
    for (auto* comp = &handler.getComponent(); comp != outermost; comp = comp->getParentComponent())
    {
        const auto result = findIndexOfComponent (comp);

        if (result != -1)
            return AccessibilityTableInterface::Span { result, 1 };
    }

    return nullopt;
}

std::unique_ptr<AccessibilityHandler> TableListBox::createAccessibilityHandler()
{
    class TableInterface final : public AccessibilityTableInterface
    {
    public:
        explicit TableInterface (TableListBox& tableListBoxToWrap)
            : tableListBox (tableListBoxToWrap)
        {
        }

        int getNumRows() const override
        {
            if (auto* tableModel = tableListBox.getTableListBoxModel())
                return tableModel->getNumRows();

            return 0;
        }

        int getNumColumns() const override
        {
            return tableListBox.getHeader().getNumColumns (true);
        }

        const AccessibilityHandler* getRowHandler (int row) const override
        {
            if (isPositiveAndBelow (row, getNumRows()))
                if (auto* rowComp = tableListBox.getComponentForRowNumber (row))
                    return rowComp->getAccessibilityHandler();

            return nullptr;
        }

        const AccessibilityHandler* getCellHandler (int row, int column) const override
        {
            if (isPositiveAndBelow (row, getNumRows()) && isPositiveAndBelow (column, getNumColumns()))
                if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, true), row))
                    return cellComponent->getAccessibilityHandler();

            return nullptr;
        }

        const AccessibilityHandler* getHeaderHandler() const override
        {
            if (tableListBox.hasAccessibleHeaderComponent())
                return tableListBox.headerComponent->getAccessibilityHandler();

            return nullptr;
        }

        Optional<Span> getRowSpan (const AccessibilityHandler& handler) const override
        {
            if (tableListBox.isParentOf (&handler.getComponent()))
                return findRecursively (handler, &tableListBox, [&] (auto* c) { return tableListBox.getRowNumberOfComponent (c); });

            return nullopt;
        }

        Optional<Span> getColumnSpan (const AccessibilityHandler& handler) const override
        {
            if (const auto rowSpan = getRowSpan (handler))
                if (auto* rowComponent = dynamic_cast<RowComp*> (tableListBox.getComponentForRowNumber (rowSpan->begin)))
                    return findRecursively (handler, &tableListBox, [&] (auto* c) { return rowComponent->getColumnNumberOfComponent (c); });

            return nullopt;
        }

        void showCell (const AccessibilityHandler& handler) const override
        {
            const auto row = getRowSpan (handler);
            const auto col = getColumnSpan (handler);

            if (row.hasValue() && col.hasValue())
            {
                tableListBox.scrollToEnsureRowIsOnscreen (row->begin);
                tableListBox.scrollToEnsureColumnIsOnscreen (col->begin);
            }
        }

    private:
        TableListBox& tableListBox;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableInterface)
    };

    return std::make_unique<AccessibilityHandler> (*this,
                                                   AccessibilityRole::table,
                                                   AccessibilityActions{},
                                                   AccessibilityHandler::Interfaces { std::make_unique<TableInterface> (*this) });
}

//==============================================================================
void TableListBoxModel::cellClicked (int, int, const MouseEvent&)       {}
void TableListBoxModel::cellDoubleClicked (int, int, const MouseEvent&) {}
void TableListBoxModel::backgroundClicked (const MouseEvent&)           {}
void TableListBoxModel::sortOrderChanged (int, bool)                    {}
int TableListBoxModel::getColumnAutoSizeWidth (int)                     { return 0; }
void TableListBoxModel::selectedRowsChanged (int)                       {}
void TableListBoxModel::deleteKeyPressed (int)                          {}
void TableListBoxModel::returnKeyPressed (int)                          {}
void TableListBoxModel::listWasScrolled()                               {}

String TableListBoxModel::getCellTooltip (int /*rowNumber*/, int /*columnId*/)    { return {}; }
var TableListBoxModel::getDragSourceDescription (const SparseSet<int>&)           { return {}; }

Component* TableListBoxModel::refreshComponentForCell (int, int, bool, [[maybe_unused]] Component* existingComponentToUpdate)
{
    jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components
    return nullptr;
}

} // namespace juce
